use snafu::Snafu;
use std::sync::Arc;
use tonic::async_trait;

use super::Operation;
use crate::core::error::operation_error::OperationError;
use crate::core::path_type::TypedPath;
use crate::core::step_context::StepContext;

#[derive(Debug, Snafu)]
#[snafu(visibility(pub(crate)))]
pub enum Error {
    #[snafu(display("Failed to run command {cmd:?}: {source}"))]
    Run { cmd: Cmd, source: std::io::Error },

    #[snafu(display("Run command {cmd:?} with exit code {code:?}"))]
    ExitStatus { cmd: Cmd, code: Option<i32> },
}

impl From<Error> for OperationError {
    fn from(value: Error) -> Self {
        OperationError::Cmd { source: value }
    }
}

#[derive(Clone, Debug)]
pub struct Cmd {
    c: String,
    workdir: TypedPath,
    params: Vec<String>,
}

impl Cmd {
    pub fn new<'a>(c: &str, workdir: &TypedPath, params: impl Iterator<Item = &'a str>) -> Self {
        Cmd {
            c: c.to_string(),
            workdir: workdir.clone(),
            params: params.map(|s| s.to_string()).collect(),
        }
    }
}

#[async_trait]
impl Operation for Cmd {
    async fn run(&mut self, context: Arc<StepContext>) -> Result<(), OperationError> {
        let workdir = match &self.workdir {
            TypedPath::Normal(p) => p.clone(),
            TypedPath::Contextual(s) => context.job_context().path().join(s),
        };

        match context
            .run_cmd_at(&self.c, self.params.iter().map(|s| s.as_str()), &workdir)
            .await
        {
            Ok(output) => {
                if output.status.success() {
                    Ok(())
                } else {
                    Err(Error::ExitStatus {
                        cmd: self.clone(),
                        code: output.status.code(),
                    }
                    .into())
                }
            }
            Err(e) => Err(Error::Run {
                cmd: self.clone(),
                source: e,
            }
            .into()),
        }
    }
}
